/*
  Copyright 2013 the JSDoc Authors.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      https://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

/**
 * Traversal utilities for ASTs that are compatible with the ESTree API.
 */

import * as astNode from './ast-node.js';
import { Syntax } from './syntax.js';

// TODO: docs
function getCurrentScope(scopes) {
  return scopes[scopes.length - 1] || null;
}

// TODO: docs
function moveLeadingComments(source, target, count) {
  if (source.leadingComments) {
    if (count === undefined) {
      count = source.leadingComments.length;
    }

    target.leadingComments = source.leadingComments.slice(0, count);
    source.leadingComments = source.leadingComments.slice(count);
  }
}

// TODO: docs
function moveTrailingComments(source, target, count) {
  if (source.trailingComments) {
    if (count === undefined) {
      count = source.trailingComments.length;
    }

    target.trailingComments = source.trailingComments.slice(
      source.trailingComments.length - count,
      count
    );
    source.trailingComments = source.trailingComments.slice();
  }
}

// eslint-disable-next-line no-empty-function, no-unused-vars
function leafNode(node, parent, state, cb) {}

// TODO: docs
export const walkers = {};

walkers[Syntax.ArrayExpression] = (node, parent, state, cb) => {
  for (let element of node.elements) {
    if (element) {
      cb(element, node, state);
    }
  }
};

// TODO: verify correctness
walkers[Syntax.ArrayPattern] = (node, parent, state, cb) => {
  for (let element of node.elements) {
    // must be an identifier or an expression
    if (element && element.type !== Syntax.Identifier) {
      cb(element, node, state);
    }
  }
};

walkers[Syntax.ArrowFunctionExpression] = (node, parent, state, cb) => {
  if (node.id) {
    cb(node.id, node, state);
  }

  for (let param of node.params) {
    cb(param, node, state);
  }

  cb(node.body, node, state);
};

walkers[Syntax.AssignmentExpression] = (node, parent, state, cb) => {
  cb(node.left, node, state);
  cb(node.right, node, state);
};

walkers[Syntax.AssignmentPattern] = walkers[Syntax.AssignmentExpression];

walkers[Syntax.AwaitExpression] = (node, parent, state, cb) => {
  cb(node.argument, node, state);
};

walkers[Syntax.BigIntLiteral] = leafNode;

walkers[Syntax.BinaryExpression] = (node, parent, state, cb) => {
  cb(node.left, node, state);
  cb(node.right, node, state);
};

walkers[Syntax.BindExpression] = (node, parent, state, cb) => {
  if (node.object) {
    cb(node.object, node, state);
  }

  cb(node.callee, node, state);
};

walkers[Syntax.BlockStatement] = (node, parent, state, cb) => {
  for (let bodyItem of node.body) {
    cb(bodyItem, node, state);
  }
};

walkers[Syntax.BreakStatement] = leafNode;

walkers[Syntax.CallExpression] = function (node, parent, state, cb) {
  cb(node.callee, node, state);

  if (node.arguments) {
    for (let arg of node.arguments) {
      cb(arg, node, state);
    }
  }
};

walkers[Syntax.CatchClause] = leafNode;

walkers[Syntax.ClassBody] = walkers[Syntax.BlockStatement];

walkers[Syntax.ClassDeclaration] = (node, parent, state, cb) => {
  if (node.id) {
    cb(node.id, node, state);
  }

  if (node.superClass) {
    cb(node.superClass, node, state);
  }

  if (node.body) {
    cb(node.body, node, state);
  }

  if (node.decorators) {
    for (let decorator of node.decorators) {
      cb(decorator, node, state);
    }
  }
};

walkers[Syntax.ClassExpression] = walkers[Syntax.ClassDeclaration];

// walkers[Syntax.ClassPrivateProperty] is defined later

// walkers[Syntax.ClassProperty] is defined later

// TODO: verify correctness
walkers[Syntax.ComprehensionBlock] = walkers[Syntax.AssignmentExpression];

// TODO: verify correctness
walkers[Syntax.ComprehensionExpression] = (node, parent, state, cb) => {
  cb(node.body, node, state);

  if (node.filter) {
    cb(node.filter, node, state);
  }

  for (let block of node.blocks) {
    cb(block, node, state);
  }
};

walkers[Syntax.ConditionalExpression] = (node, parent, state, cb) => {
  cb(node.test, node, state);
  cb(node.consequent, node, state);
  cb(node.alternate, node, state);
};

walkers[Syntax.ContinueStatement] = leafNode;

walkers[Syntax.DebuggerStatement] = leafNode;

walkers[Syntax.Decorator] = (node, parent, state, cb) => {
  cb(node.expression, node, state);
};

walkers[Syntax.DoExpression] = (node, parent, state, cb) => {
  cb(node.body, node, state);
};

walkers[Syntax.DoWhileStatement] = (node, parent, state, cb) => {
  cb(node.test, node, state);
  cb(node.body, node, state);
};

walkers[Syntax.EmptyStatement] = leafNode;

walkers[Syntax.ExperimentalRestProperty] = (node, parent, state, cb) => {
  cb(node.argument, node, state);
};

walkers[Syntax.ExperimentalSpreadProperty] = walkers[Syntax.ExperimentalRestProperty];

walkers[Syntax.ExportAllDeclaration] = (node, parent, state, cb) => {
  if (node.source) {
    cb(node.source, node, state);
  }
};

walkers[Syntax.ExportDefaultDeclaration] = (node, parent, state, cb) => {
  // if the declaration target is a class, move leading comments to the declaration target
  if (node.declaration && node.declaration.type === Syntax.ClassDeclaration) {
    moveLeadingComments(node, node.declaration);
  }

  if (node.declaration) {
    cb(node.declaration, node, state);
  }
};

walkers[Syntax.ExportDefaultSpecifier] = (node, parent, state, cb) => {
  cb(node.exported, node, state);
};

walkers[Syntax.ExportNamedDeclaration] = (node, parent, state, cb) => {
  if (node.declaration) {
    cb(node.declaration, node, state);
  }

  for (let specifier of node.specifiers) {
    cb(specifier, node, state);
  }

  if (node.source) {
    cb(node.source, node, state);
  }
};

walkers[Syntax.ExportNamespaceSpecifier] = (node, parent, state, cb) => {
  cb(node.exported, node, state);
};

walkers[Syntax.ExportSpecifier] = (node, parent, state, cb) => {
  if (node.exported) {
    cb(node.exported, node, state);
  }

  if (node.local) {
    cb(node.local, node, state);
  }
};

walkers[Syntax.ExpressionStatement] = (node, parent, state, cb) => {
  moveLeadingComments(node, node.expression);

  cb(node.expression, node, state);
};

walkers[Syntax.File] = (node, parent, state, cb) => {
  cb(node.program, node, state);
};

walkers[Syntax.ForInStatement] = (node, parent, state, cb) => {
  cb(node.left, node, state);
  cb(node.right, node, state);
  cb(node.body, node, state);
};

walkers[Syntax.ForOfStatement] = walkers[Syntax.ForInStatement];

walkers[Syntax.ForStatement] = (node, parent, state, cb) => {
  if (node.init) {
    cb(node.init, node, state);
  }

  if (node.test) {
    cb(node.test, node, state);
  }

  if (node.update) {
    cb(node.update, node, state);
  }

  cb(node.body, node, state);
};

walkers[Syntax.FunctionDeclaration] = walkers[Syntax.ArrowFunctionExpression];

walkers[Syntax.FunctionExpression] = walkers[Syntax.ArrowFunctionExpression];

walkers[Syntax.Identifier] = leafNode;

walkers[Syntax.IfStatement] = (node, parent, state, cb) => {
  cb(node.test, node, state);
  cb(node.consequent, node, state);
  if (node.alternate) {
    cb(node.alternate, node, state);
  }
};

walkers[Syntax.Import] = leafNode;

walkers[Syntax.ImportDeclaration] = (node, parent, state, cb) => {
  if (node.specifiers) {
    for (let specifier of node.specifiers) {
      cb(specifier, node, state);
    }
  }

  if (node.source) {
    cb(node.source, node, state);
  }
};

walkers[Syntax.ImportDefaultSpecifier] = (node, parent, state, cb) => {
  if (node.local) {
    cb(node.local, node, state);
  }
};

walkers[Syntax.ImportNamespaceSpecifier] = walkers[Syntax.ImportDefaultSpecifier];

walkers[Syntax.ImportSpecifier] = walkers[Syntax.ExportSpecifier];

walkers[Syntax.JSXAttribute] = (node, parent, state, cb) => {
  cb(node.name, node, state);

  if (node.value) {
    cb(node.value, node, state);
  }
};

walkers[Syntax.JSXClosingElement] = (node, parent, state, cb) => {
  cb(node.name, node, state);
};

walkers[Syntax.JSXElement] = (node, parent, state, cb) => {
  cb(node.openingElement, node, state);

  if (node.closingElement) {
    cb(node.closingElement, node, state);
  }

  for (let child of node.children) {
    cb(child, node, state);
  }
};

walkers[Syntax.JSXEmptyExpression] = leafNode;

walkers[Syntax.JSXExpressionContainer] = (node, parent, state, cb) => {
  cb(node.expression, node, state);
};

walkers[Syntax.JSXIdentifier] = leafNode;

walkers[Syntax.JSXMemberExpression] = (node, parent, state, cb) => {
  cb(node.object, node, state);

  cb(node.property, node, state);
};

walkers[Syntax.JSXNamespacedName] = (node, parent, state, cb) => {
  cb(node.namespace, node, state);

  cb(node.name, node, state);
};

walkers[Syntax.JSXOpeningElement] = (node, parent, state, cb) => {
  cb(node.name, node, state);

  for (let attribute of node.attributes) {
    cb(attribute, node, state);
  }
};

walkers[Syntax.JSXSpreadAttribute] = (node, parent, state, cb) => {
  cb(node.argument, node, state);
};

walkers[Syntax.JSXText] = leafNode;

walkers[Syntax.LabeledStatement] = (node, parent, state, cb) => {
  cb(node.body, node, state);
};

// TODO: add scope info??
walkers[Syntax.LetStatement] = (node, parent, state, cb) => {
  for (let headItem of node.head) {
    cb(headItem.id, node, state);
    if (headItem.init) {
      cb(headItem.init, node, state);
    }
  }

  cb(node.body, node, state);
};

walkers[Syntax.Literal] = leafNode;

walkers[Syntax.LogicalExpression] = walkers[Syntax.BinaryExpression];

walkers[Syntax.MemberExpression] = (node, parent, state, cb) => {
  cb(node.object, node, state);
  if (node.property) {
    cb(node.property, node, state);
  }
};

walkers[Syntax.MetaProperty] = leafNode;

walkers[Syntax.MethodDefinition] = (node, parent, state, cb) => {
  if (node.key) {
    cb(node.key, node, state);
  }

  if (node.value) {
    cb(node.value, node, state);
  }

  if (node.decorators) {
    for (let decorator of node.decorators) {
      cb(decorator, node, state);
    }
  }
};

walkers[Syntax.ModuleDeclaration] = (node, parent, state, cb) => {
  if (node.id) {
    cb(node.id, node, state);
  }

  if (node.source) {
    cb(node.source, node, state);
  }

  if (node.body) {
    cb(node.body, node, state);
  }
};

walkers[Syntax.NewExpression] = walkers[Syntax.CallExpression];

walkers[Syntax.ObjectExpression] = (node, parent, state, cb) => {
  for (let property of node.properties) {
    cb(property, node, state);
  }
};

walkers[Syntax.ObjectPattern] = walkers[Syntax.ObjectExpression];

walkers[Syntax.PrivateName] = (node, parent, state, cb) => {
  cb(node.id, node, state);
};

walkers[Syntax.Program] = (node, parent, state, cb) => {
  // if the first item in the body has multiple leading comments, move all but the last one to
  // this node. this happens, for example, when a file has a /** @module */ standalone comment
  // followed by one or more other comments.
  if (node.body[0] && node.body[0].leadingComments && node.body[0].leadingComments.length > 1) {
    moveLeadingComments(node.body[0], node, node.body[0].leadingComments.length - 1);
  }

  // if the last item in the body has trailing comments, move them to this node
  if (node.body.length && node.body[node.body.length - 1].trailingComments) {
    moveTrailingComments(node.body[node.body.length - 1], node);
  }

  for (let bodyItem of node.body) {
    cb(bodyItem, node, state);
  }
};

walkers[Syntax.Property] = (node, parent, state, cb) => {
  // move leading comments from key to property node
  moveLeadingComments(node.key, node);

  if (node.value) {
    cb(node.value, node, state);
  }

  if (node.decorators) {
    for (let decorator of node.decorators) {
      cb(decorator, node, state);
    }
  }
};

walkers[Syntax.ClassPrivateProperty] = (node, parent, state, cb) => {
  // move leading comments from key to property node
  moveLeadingComments(node.key, node);

  // add `name` property to key, so we don't have to give this type of node special treatment
  // when we resolve its name
  node.key.name = node.key.id.name;

  if (node.value) {
    cb(node.value, node, state);
  }

  if (node.decorators) {
    for (let decorator of node.decorators) {
      cb(decorator, node, state);
    }
  }
};

walkers[Syntax.ClassProperty] = walkers[Syntax.Property];

walkers[Syntax.RestElement] = (node, parent, state, cb) => {
  if (node.argument) {
    cb(node.argument, node, state);
  }
};

walkers[Syntax.ReturnStatement] = (node, parent, state, cb) => {
  if (node.argument) {
    cb(node.argument, node, state);
  }
};

walkers[Syntax.SequenceExpression] = (node, parent, state, cb) => {
  for (let expression of node.expressions) {
    cb(expression, node, state);
  }
};

walkers[Syntax.SpreadElement] = (node, parent, state, cb) => {
  if (node.argument) {
    cb(node.argument, node, state);
  }
};

walkers[Syntax.Super] = leafNode;

walkers[Syntax.SwitchCase] = (node, parent, state, cb) => {
  if (node.test) {
    cb(node.test, node, state);
  }

  for (let consequentItem of node.consequent) {
    cb(consequentItem, node, state);
  }
};

walkers[Syntax.SwitchStatement] = (node, parent, state, cb) => {
  cb(node.discriminant, node, state);

  for (let caseItem of node.cases) {
    cb(caseItem, node, state);
  }
};

walkers[Syntax.TaggedTemplateExpression] = (node, parent, state, cb) => {
  if (node.tag) {
    cb(node.tag, node, state);
  }
  if (node.quasi) {
    cb(node.quasi, node, state);
  }
};

walkers[Syntax.TemplateElement] = leafNode;

walkers[Syntax.TemplateLiteral] = (node, parent, state, cb) => {
  if (node.quasis && node.quasis.length) {
    for (let quasi of node.quasis) {
      cb(quasi, node, state);
    }
  }

  if (node.expressions && node.expressions.length) {
    for (let expression of node.expressions) {
      cb(expression, node, state);
    }
  }
};

walkers[Syntax.ThisExpression] = leafNode;

walkers[Syntax.ThrowStatement] = (node, parent, state, cb) => {
  cb(node.argument, node, state);
};

walkers[Syntax.TryStatement] = (node, parent, state, cb) => {
  cb(node.block, node, state);

  if (node.handler) {
    cb(node.handler.body, node, state);
  }

  if (node.finalizer) {
    cb(node.finalizer, node, state);
  }
};

walkers[Syntax.UnaryExpression] = (node, parent, state, cb) => {
  cb(node.argument, node, state);
};

walkers[Syntax.UpdateExpression] = walkers[Syntax.UnaryExpression];

walkers[Syntax.VariableDeclaration] = (node, parent, state, cb) => {
  // move leading comments to first declarator
  moveLeadingComments(node, node.declarations[0]);

  for (let declaration of node.declarations) {
    cb(declaration, node, state);
  }
};

walkers[Syntax.VariableDeclarator] = (node, parent, state, cb) => {
  cb(node.id, node, state);

  if (node.init) {
    cb(node.init, node, state);
  }
};

walkers[Syntax.WhileStatement] = walkers[Syntax.DoWhileStatement];

walkers[Syntax.WithStatement] = (node, parent, state, cb) => {
  cb(node.object, node, state);
  cb(node.body, node, state);
};

walkers[Syntax.YieldExpression] = (node, parent, state, cb) => {
  if (node.argument) {
    cb(node.argument, node, state);
  }
};

function handleNode(node, parent, cbState) {
  let currentScope;
  const isScope = astNode.isScope(node);
  let moduleType;
  const { walker } = cbState;

  astNode.addNodeProperties(node);
  node.parent = parent || null;

  currentScope = getCurrentScope(cbState.scopes);
  if (currentScope) {
    node.enclosingScope = currentScope;
  }

  if (isScope) {
    cbState.scopes.push(node);
  }
  cbState.nodes.push(node);

  if (!cbState.moduleType) {
    moduleType = cbState.moduleType = astNode.detectModuleType(node);
    if (moduleType) {
      cbState.moduleTypes.set(cbState.filename, moduleType);
    }
  }

  if (!walker._walkers[node.type]) {
    walker._logUnknownNodeType(node);
  } else {
    walker._walkers[node.type](node, parent, cbState, handleNode);
  }

  if (isScope) {
    cbState.scopes.pop();
  }
}

/**
 * A walker that can traverse an ESTree AST.
 */
export class Walker {
  // TODO: docs
  constructor(env, { moduleTypes }, walkerFuncs = walkers) {
    this._log = env.log;
    this._moduleTypes = moduleTypes;
    this._walkers = walkerFuncs;
  }

  _logUnknownNodeType({ type }) {
    this._log.debug(
      `Found a node with unrecognized type ${type}. Ignoring the node and its descendants.`
    );
  }

  // TODO: docs
  recurse(ast, visitor, filename) {
    let shouldContinue;
    const state = {
      filename: filename,
      moduleType: null,
      moduleTypes: this._moduleTypes,
      nodes: [],
      scopes: [],
      walker: this,
    };

    handleNode(ast, null, state);

    if (visitor) {
      for (let node of state.nodes) {
        shouldContinue = visitor.visit(node, filename);
        if (!shouldContinue) {
          break;
        }
      }
    }

    return {
      ast,
      filename: state.filename,
      moduleType: state.moduleType,
    };
  }
}
